﻿using Microsoft.VisualStudio.TextTemplating;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
using System.IO;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;


namespace Salient.SqlServer
{
    public class Config
    {
        public Config(ITextTemplatingEngineHost host) : this(host, null) { }
        public Config(ITextTemplatingEngineHost host, string configurationFilename)
        {

            // Get the instance of Visual Studio that is hosting the calling file
            EnvDTE.DTE env = (EnvDTE.DTE)((IServiceProvider)host).GetService(typeof(EnvDTE.DTE));
            // Gets an array of currently selected projects. Since you are either in this file saving it or
            // right-clicking the item in solution explorer to invoke the context menu it stands to reason
            // that there is 1 ActiveSolutionProject and that it is the parent of this file....
            _project = (EnvDTE.Project)((Array)env.ActiveSolutionProjects).GetValue(0);



            if (string.IsNullOrEmpty(configurationFilename))
            {
                // examine each project item's filename looking for app.config or web.config
                foreach (EnvDTE.ProjectItem item in Project.ProjectItems)
                {

                    if (Regex.IsMatch(item.Name, "(app|web).config", RegexOptions.IgnoreCase))
                    {
                        // TODO: try this with linked files. is the filename pointing to the source?
                        configurationFilename = item.get_FileNames(0);
                        break;
                    }
                }
            }

            if (!string.IsNullOrEmpty(configurationFilename))
            {
                // found it, map it and return it
                ExeConfigurationFileMap configFile = null;
                configFile = new ExeConfigurationFileMap();
                configFile.ExeConfigFilename = configurationFilename;
                _config = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFile, ConfigurationUserLevel.None);
            }
        }



        public List<EnvDTE.ProjectItem> FindItem(EnvDTE.ProjectItems projectItems, Regex rx)
        {

            List<EnvDTE.ProjectItem> returnValue = new List<EnvDTE.ProjectItem>();
            // examine each project item's filename looking for app.config or web.config
            foreach (EnvDTE.ProjectItem item in projectItems)
            {
                if (rx.IsMatch(item.Name))
                {
                    returnValue.Add(item);
                }
                var prj = item.ProjectItems as EnvDTE.ProjectItems;
                if (prj != null)
                {
                    returnValue.AddRange(FindItem(prj, rx));
                }

            }
            return returnValue;
        }
        private Configuration _config;
        private EnvDTE.Project _project;

        /// <summary>
        /// Example:
        /// <code>
        /// string cs = ConnectionStrings["myConnectionString"].ConnectionString;
        /// </code>
        /// </summary>
        public ConnectionStringSettingsCollection ConnectionStrings
        {
            get { return Configuration.ConnectionStrings.ConnectionStrings; }
        }

        /// <summary>
        /// Example:
        /// <code>
        /// string setting = AppSettings["mySetting"].Value;
        /// </code>
        /// </summary>
        public KeyValueConfigurationCollection AppSettings
        {
            get { return Configuration.AppSettings.Settings; }
        }


        /// <summary>
        /// Returns value of setting, if present, otherwise defaultValue
        /// </summary>
        /// <param name="key">appSettings key</param>
        /// <param name="defaultValue">value to return if setting is absent</param>
        /// <returns></returns>
        public object AppSettingValueOrDefault(string key, object defaultValue)
        {
            if (AppSettings[key] != null)
            {
                return AppSettings[key].Value;
            }
            return defaultValue;
        }


        /// <summary>
        /// The app/web config file for hosting project, if any
        /// </summary>
        public Configuration Configuration
        {
            get
            {
                return _config;
            }
        }

        /// <summary>
        /// The hosting project.
        /// </summary>
        public EnvDTE.Project Project
        {
            get
            {
                return _project;
            }
        }
    }


    /// <summary>
    /// 
    /// </summary>
    public class DeadSimpleRenderer
    {
        public DeadSimpleRenderer(TextTransformation template, DeadSimpleConfig parameters)
        {
            _parameters = parameters;
            _template = template;
            _scriptPath = System.IO.Path.GetDirectoryName(template.Host.TemplateFile) + "\\DeadSimpleDTO2.sql";
        }

        private DeadSimpleConfig _parameters;
        private TextTransformation _template;
        private string _scriptPath = string.Empty;

        public TextTransformation Template
        {
            get { return _template; }
        }
        public string ScriptPath
        {
            get { return _scriptPath; }
            set { _scriptPath = value; }
        }
        public virtual void Render()
        {

            var cfg = new Config(_template.Host);
            string cString = _parameters.GetConnectionString();
            if (cfg.ConnectionStrings[cString] != null)
            {
                cString = cfg.ConnectionStrings[cString].ConnectionString;
            }

            using (SqlConnection conn = new SqlConnection(cString))
            {
                conn.Open();

                // execute the script
                using (var cmd = conn.CreateCommand())
                {

                    string sql = System.IO.File.ReadAllText(_scriptPath);
                    cmd.CommandText = sql;
                    _parameters.SetParameters(cmd);


                    // inline anonymous delegate to catch the output
                    string output = null;
                    conn.InfoMessage += ((ignored, messageArgs) => { output = messageArgs.Message; });

                    SqlDataReader reader = cmd.ExecuteReader();
                    var table = new DataTable();
                    table.Load(reader);
                    reader.Close();
                    DataRow[] namespaceRows = table.Select("name='NAMESPACE_OPEN' or name='NAMESPACE_CLOSE'", "name desc");

                    // expecting 2, open and close

                    if (namespaceRows.Length != 2)
                    {
                        _template.WriteLine(output);
                        throw new InvalidOperationException("Did not get expected namespace blocks back.");
                    }
                    string namespaceOpen = (string)namespaceRows[0]["line"];
                    string namespaceClose = (string)namespaceRows[1]["line"];

                    table.Rows.Remove(namespaceRows[0]);
                    table.Rows.Remove(namespaceRows[1]);

                    var manager = Manager.Create(_template.Host, _template.GenerationEnvironment);

                    manager.StartHeader();
                    _template.WriteLine(namespaceOpen);
                    manager.EndBlock();


                    string currentName = null;
                    foreach (DataRow row in table.Rows)
                    {
                        if (!row.IsNull("name"))
                        {
                            if (currentName != null)
                            {
                                manager.EndBlock();
                                currentName = null;
                            }
                            currentName = (string)row["name"];
                            manager.StartNewFile(currentName + ".generated.cs");
                            // start a new file
                        }
                        if (!row.IsNull("line"))
                        {
                            // add to current file
                            _template.WriteLine((string)row["line"]);
                        }

                    }
                    // finish up the last file
                    if (currentName != null)
                    {
                        manager.EndBlock();
                        currentName = null;
                    }

                    manager.StartFooter();
                    _template.WriteLine(namespaceClose);
                    manager.EndBlock();

                    if (_parameters.renderSeparateFiles)
                    {
                        manager.Process(true);
                    }
                }
            }
        }
    }

    public class Manager
    {
        // from http://damieng.com/blog/2009/11/06/multiple-outputs-from-t4-made-easy-revisited#comments

        // NOTE: not sure what it means but this license header was present in the previous version but
        // omitted in the version presented here.... I am just including it out of common sense. you
        // do what you want.

        // T4 Template Block manager for handling multiple file outputs more easily.
        // Copyright (c) Microsoft Corporation.  All rights reserved.
        // This source code is made available under the terms of the Microsoft Public License (MS-PL)	

        private class Block
        {
            public String Name;
            public int Start, Length;
        }

        private Block currentBlock;
        private List<Block> files = new List<Block>();
        private Block footer = new Block();
        private Block header = new Block();
        private ITextTemplatingEngineHost host;
        private StringBuilder template;
        protected List<String> generatedFileNames = new List<String>();

        public static Manager Create(ITextTemplatingEngineHost host, StringBuilder template)
        {
            return (host is IServiceProvider) ? new VSManager(host, template) : new Manager(host, template);
        }

        public void StartNewFile(String name)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            CurrentBlock = new Block { Name = name };
        }

        public void StartFooter()
        {
            CurrentBlock = footer;
        }

        public void StartHeader()
        {
            CurrentBlock = header;
        }

        public void EndBlock()
        {
            if (CurrentBlock == null)
                return;
            CurrentBlock.Length = template.Length - CurrentBlock.Start;
            if (CurrentBlock != header && CurrentBlock != footer)
                files.Add(CurrentBlock);
            currentBlock = null;
        }

        public virtual void Process(bool split)
        {
            if (split)
            {
                EndBlock();
                String headerText = template.ToString(header.Start, header.Length);
                String footerText = template.ToString(footer.Start, footer.Length);
                String outputPath = Path.GetDirectoryName(host.TemplateFile);
                files.Reverse();
                foreach (Block block in files)
                {
                    String fileName = Path.Combine(outputPath, block.Name);
                    String content = headerText + template.ToString(block.Start, block.Length) + footerText;
                    generatedFileNames.Add(fileName);
                    CreateFile(fileName, content);
                    template.Remove(block.Start, block.Length);
                }
            }
        }

        protected virtual void CreateFile(String fileName, String content)
        {
            if (IsFileContentDifferent(fileName, content))
                File.WriteAllText(fileName, content);
        }

        public virtual String GetCustomToolNamespace(String fileName)
        {
            return null;
        }

        public virtual String DefaultProjectNamespace
        {
            get { return null; }
        }

        protected bool IsFileContentDifferent(String fileName, String newContent)
        {
            return !(File.Exists(fileName) && File.ReadAllText(fileName) == newContent);
        }

        private Manager(ITextTemplatingEngineHost host, StringBuilder template)
        {
            this.host = host;
            this.template = template;
        }

        private Block CurrentBlock
        {
            get { return currentBlock; }
            set
            {
                if (CurrentBlock != null)
                    EndBlock();
                if (value != null)
                    value.Start = template.Length;
                currentBlock = value;
            }
        }

        private class VSManager : Manager
        {
            private EnvDTE.ProjectItem templateProjectItem;
            private EnvDTE.DTE dte;
            private Action<String> checkOutAction;
            private Action<IEnumerable<String>> projectSyncAction;

            public override String DefaultProjectNamespace
            {
                get
                {
                    return templateProjectItem.ContainingProject.Properties.Item("DefaultNamespace").Value.ToString();
                }
            }

            public override String GetCustomToolNamespace(string fileName)
            {
                return dte.Solution.FindProjectItem(fileName).Properties.Item("CustomToolNamespace").Value.ToString();
            }

            public override void Process(bool split)
            {
                if (templateProjectItem.ProjectItems == null)
                    return;
                base.Process(split);
                projectSyncAction.EndInvoke(projectSyncAction.BeginInvoke(generatedFileNames, null, null));
            }

            protected override void CreateFile(String fileName, String content)
            {
                if (IsFileContentDifferent(fileName, content))
                {
                    CheckoutFileIfRequired(fileName);
                    File.WriteAllText(fileName, content);
                }
            }

            internal VSManager(ITextTemplatingEngineHost host, StringBuilder template)
                : base(host, template)
            {
                var hostServiceProvider = (IServiceProvider)host;
                if (hostServiceProvider == null)
                    throw new ArgumentNullException("Could not obtain IServiceProvider");
                dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
                if (dte == null)
                    throw new ArgumentNullException("Could not obtain DTE from host");
                templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
                checkOutAction = (String fileName) => dte.SourceControl.CheckOutItem(fileName);
                projectSyncAction = (IEnumerable<String> keepFileNames) => ProjectSync(templateProjectItem, keepFileNames);
            }

            private static void ProjectSync(EnvDTE.ProjectItem templateProjectItem, IEnumerable<String> keepFileNames)
            {
                var keepFileNameSet = new HashSet<String>(keepFileNames);
                var projectFiles = new Dictionary<String, EnvDTE.ProjectItem>();
                var originalFilePrefix = Path.GetFileNameWithoutExtension(templateProjectItem.get_FileNames(0)) + ".";
                foreach (EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
                    projectFiles.Add(projectItem.get_FileNames(0), projectItem);

                // Remove unused items from the project
                foreach (var pair in projectFiles)
                    if (!keepFileNames.Contains(pair.Key) && !(Path.GetFileNameWithoutExtension(pair.Key) + ".").StartsWith(originalFilePrefix))
                        pair.Value.Delete();

                // Add missing files to the project
                foreach (String fileName in keepFileNameSet)
                    if (!projectFiles.ContainsKey(fileName))
                        templateProjectItem.ProjectItems.AddFromFile(fileName);
            }

            private void CheckoutFileIfRequired(String fileName)
            {
                var sc = dte.SourceControl;
                if (sc != null && sc.IsItemUnderSCC(fileName) && !sc.IsItemCheckedOut(fileName))
                    checkOutAction.EndInvoke(checkOutAction.BeginInvoke(fileName, null, null));
            }
        }
    }






    /// <summary>
    /// 
    /// </summary>
    public class DeadSimpleConfig
    {
        public DeadSimpleConfig(TextTransformation template, string prefix)
            : this()
        {
            this.Load(template, prefix);
        }

        public DeadSimpleConfig(TextTransformation template, string prefix, string configurationFilePath)
            : this()
        {
            this.Load(template, prefix, configurationFilePath);
        }
        public DeadSimpleConfig()
        {
            // defaults
            _listInstanceType = "List";
            _listMemberType = "IList";
            _commonBaseName = "DeadSimpleDTO";


            _renderOneToManyRelations = true;
            _renderManyToManyRelations = true;
            _renderManyToOneRelations = true;
            _renderDomainObjects = true;
            _returnResultSet = true;
        }

        private string _commonBaseName;
        private string _connectionString;
        private string _dataContractNamespace;
        private bool _eagerLoadEntities;
        private bool _eagerLoadRelations;
        private bool _hideReadOnlySetters;
        private string _identifierMode;
        private string _listInstanceType;
        private string _listMemberType;
        private string _memberNameSubstitutions;
        private string _membersToExclude;
        private string _namespacesToInclude;
        private string _objectsToExclude;
        private string _objectsToRender;
        private bool _previousBool = true;
        private string _referenceTypes;
        private bool _renderCommonBase;
        private bool _renderDataContract;
        private bool _renderDomainObjects;
        private bool _renderFluentNHibernate;
        private bool _renderINotify;
        private bool _renderLoaderAttributes;
        private bool _renderManyToManyRelations;
        private bool _renderManyToOneRelations;
        private bool _renderOneToManyRelations;
        private bool _renderSchemaNamespace;
        private bool _renderSeparateFiles;
        private bool _renderSerializable;
        private bool _returnResultSet;
        private string _rootNamespace;
        private string _typeSubstitutions;

        public string GetConnectionString()
        {
            return _connectionString;
        }
        public bool renderSeparateFiles
        {
            get { return _renderSeparateFiles; }
        }
        public DeadSimpleConfig ReferenceTypes(string csv)
        {
            _referenceTypes = csv;
            return this;
        }

        public DeadSimpleConfig TypeSubstitutions(string pcsv)
        {
            _typeSubstitutions = pcsv;
            return this;
        }

        public DeadSimpleConfig CommonBaseName(string name)
        {
            _commonBaseName = name;
            return this;
        }

        public DeadSimpleConfig MemberNameSubstitutions(string pcsv)
        {
            _memberNameSubstitutions = pcsv;
            return this;
        }

        public DeadSimpleConfig DataContractNamespace(string name)
        {
            _renderDataContract = true;
            _dataContractNamespace = name;
            return this;
        }

        public DeadSimpleConfig MembersToExclude(string csv)
        {
            _membersToExclude = csv;
            return this;
        }

        public DeadSimpleConfig RootNamespace(string name)
        {
            _rootNamespace = name;
            return this;
        }

        public DeadSimpleConfig ObjectsToRender(string csv)
        {
            _objectsToRender = csv;
            return this;
        }

        public DeadSimpleConfig ObjectsToExclude(string csv)
        {
            _objectsToExclude = csv;
            return this;
        }

        public DeadSimpleConfig IdentifierMode(string value)
        {
            _identifierMode = value;
            return this;
        }

        public DeadSimpleConfig ListInstanceType(string value)
        {
            _listInstanceType = value;
            return this;
        }

        public DeadSimpleConfig ListMemberType(string value)
        {
            _listMemberType = value;
            return this;
        }

        public DeadSimpleConfig NamespacesToInclude(string value)
        {
            _namespacesToInclude = value;
            return this;
        }

        public DeadSimpleConfig ConnectionString(string value)
        {
            _connectionString = value;
            return this;
        }

        public DeadSimpleConfig Not()
        {
            _previousBool = false;
            return this;
        }

        public DeadSimpleConfig With(Action<DeadSimpleConfig> action)
        {
            action.Invoke(this);
            return this;
        }

        public DeadSimpleConfig FluentNHibernate()
        {
            _renderFluentNHibernate = _previousBool;
            _previousBool = true;
            return this;
        }


        public DeadSimpleConfig SchemaNamespace()
        {
            _renderSchemaNamespace = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig ProtectedSetters()
        {
            _hideReadOnlySetters = _previousBool;
            _previousBool = true;
            return this;
        }
        public DeadSimpleConfig DataContract(string nspace)
        {
            _dataContractNamespace = nspace;
            _renderDataContract = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig DataContract()
        {
            _renderDataContract = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig Serializable()
        {
            _renderSerializable = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig DomainObjects()
        {
            _renderDomainObjects = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig EagerEntities()
        {
            _eagerLoadEntities = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig EagerRelations()
        {
            _eagerLoadRelations = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig OneToMany()
        {
            _renderOneToManyRelations = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig ManyToOne()
        {
            _renderManyToOneRelations = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig ManyToMany()
        {
            _renderManyToManyRelations = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig CommonBase(string name)
        {
            _renderCommonBase = true;
            _commonBaseName = name;
            return this;
        }

        public DeadSimpleConfig CommonBase()
        {
            _renderCommonBase = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig LoaderAttributes()
        {
            _renderLoaderAttributes = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig INotify()
        {
            _renderINotify = _previousBool;
            _previousBool = true;
            return this;
        }

        public DeadSimpleConfig ResultSet()
        {
            _returnResultSet = _previousBool;
            _previousBool = true;
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        public DeadSimpleConfig SeparateFiles()
        {
            _renderSeparateFiles = _previousBool;
            _previousBool = true;
            return this;
        }


        public void Load(TextTransformation template)
        {
            Load(template, null);
        }
        public void Load(TextTransformation template, string prefix)
        {
            Load(template, prefix, null);
        }
        public void Load(TextTransformation template, string prefix, string configurationFilePath)
        {
            if (string.IsNullOrEmpty(prefix))
            {
                prefix = "DSDTO2-";
            }

            Config config = new Config(template.Host, configurationFilePath);

            if (string.IsNullOrEmpty(this._rootNamespace))
            {
                this._rootNamespace = config.Project.Properties.Item("DefaultNamespace").Value.ToString();
            }

            var fields = this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.GetField);

            foreach (FieldInfo field in fields)
            {
                var value = field.GetValue(this);
                var configValue = config.AppSettingValueOrDefault(prefix + field.Name.Substring(1), value);
                if (field.FieldType == typeof(string))
                {
                }
                else if (field.FieldType == typeof(bool))
                {
                    configValue = Convert.ToBoolean(configValue);
                }
                else
                {
                    throw new InvalidOperationException("supports value types and string only");
                }


                field.SetValue(this, configValue);
            }

        }
        public void SetParameters(SqlCommand cmd)
        {
            var fields = this.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
            foreach (FieldInfo field in fields)
            {
                var value = field.GetValue(this);
                if (value == null)
                {
                    if (field.FieldType == typeof(string))
                    {
                        value = string.Empty;
                    }
                    else
                    {
                        throw new InvalidOperationException("supports value types and string only");
                    }
                }
                Console.WriteLine("@" + field.Name.Substring(1));
                cmd.Parameters.AddWithValue("@" + field.Name.Substring(1), value);
            }
        }
    }	
}
